package com.sneakxy.mybatis.commons.orm;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.builder.ResultMapResolver;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.repository.core.RepositoryInformation;

import com.sneakxy.mybatis.commons.annotations.Column;
import com.sneakxy.mybatis.commons.annotations.Table;
import com.sneakxy.mybatis.commons.orm.generation.GenerationManager;
import com.sneakxy.mybatis.commons.orm.mapping.MybatisColumn;
import com.sneakxy.mybatis.commons.repository.config.MybatisCommonsConfiguration;
import com.sneakxy.mybatis.commons.repository.query.parser.NullTypeHandler;
import com.sneakxy.mybatis.commons.repository.support.MybatisEntityInformation;
import com.sneakxy.mybatis.commons.utils.MybatisUtils;

/**
 * Engity管理器
 * @author 潜行的青衣
 */
public class MybatisEntityManager {

	@SuppressWarnings("rawtypes")
	private MybatisEntityInformation entityInformation;
	
	private SqlSessionTemplate sqlSessionTemplate;
	
	private MybatisCommonsConfiguration configuration;
	
	private RepositoryInformation repositoryInformation;
	
	private MapperBuilderAssistant assistant;
	
	private GenerationManager generationManager;
	
	@SuppressWarnings("rawtypes")
	public MybatisEntityManager(
			RepositoryInformation repositoryInformation,
			MybatisEntityInformation entityInformation, 
			SqlSessionTemplate sqlSessionTemplate,
			MybatisCommonsConfiguration configuration) {
		this.repositoryInformation = repositoryInformation;
		this.entityInformation = entityInformation;
		this.sqlSessionTemplate = sqlSessionTemplate;
		this.configuration = configuration;
		this.generationManager = new GenerationManager(this);
		this.init();
		this.initEntityResultMap();
	}
	
	@SuppressWarnings("rawtypes")
	public MybatisEntityInformation getEntityInformation() {
		return entityInformation;
	}

	public SqlSessionTemplate getSqlSessionTemplate() {
		return sqlSessionTemplate;
	}

	public MybatisCommonsConfiguration getConfiguration() {
		return configuration;
	}

	public RepositoryInformation getRepositoryInformation() {
		return repositoryInformation;
	}
	
	public String getNamespacedId(String name) {
		return new StringBuilder()
				.append(this.assistant.getCurrentNamespace())
				.append(".")
				.append(name).toString();
	}
	
	public String getCurrentNamespace() {
		return this.assistant.getCurrentNamespace();
	}
	
	public String getEntityResultMapId() {
		return this.getNamespacedId(entityInformation.getJavaType().getSimpleName());
	}
	
	public String getTablename() {
		Class<?> domainClass = this.getEntityInformation().getJavaType();
		String tableName = this.getEntityInformation().getJavaType().getSimpleName();
		Table table = domainClass.getAnnotation(Table.class);
		if(table != null) {
			tableName = this.getAttribute(table.value(), tableName);
		}
		return tableName;
	}
	
	public void addMappedStatement(
			String id, SqlSource sqlSource, SqlCommandType sqlCommandType, 
			Class<?> parameterType, Class<?> resultType) {
		this.addMappedStatement(id, sqlSource, sqlCommandType, null, parameterType, null, resultType);
	}
	
	public void addMappedStatement(
			String id, SqlSource sqlSource, SqlCommandType sqlCommandType, 
			String parameterMapName, String resultMapName, Class<?> resultType) {
		this.addMappedStatement(id, sqlSource, sqlCommandType, parameterMapName, null, resultMapName, resultType);
	}
	
	public void addMappedStatement(
			String id, SqlSource sqlSource, SqlCommandType sqlCommandType, 
			Class<?> parameterType, String resultMapName, Class<?> resultType) {
		this.addMappedStatement(id, sqlSource, sqlCommandType, null, parameterType, resultMapName, resultType);
	}
	
	public void addMappedStatement(
			String id, SqlSource sqlSource, SqlCommandType sqlCommandType, 
			String parameterMapName,  Class<?> resultType) {
		this.addMappedStatement(id, sqlSource, sqlCommandType, parameterMapName, null, null, resultType);
	}
	
	public void addMappedStatement(
			String id, SqlSource sqlSource, SqlCommandType sqlCommandType, 
			String parameterMapName, Class<?> parameterType,
			String resultMapName, Class<?> resultType) {
		Integer fetchSize = null;
		Integer timeout = null;
		StatementType statementType = StatementType.PREPARED;
      	ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      	boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        boolean flushCache = !isSelect;
        boolean useCache = isSelect;
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        String keyProperty = "id";
        String keyColumn = null;
		this.assistant.addMappedStatement(
				id, 
				sqlSource, 
				statementType, 
				sqlCommandType, 
				fetchSize, 
				timeout,
				parameterMapName, 
				parameterType, 
				resultMapName, 
				resultType, 
				resultSetType, 
				flushCache, 
				useCache, 
				false,
				keyGenerator, 
				keyProperty, 
				keyColumn, 
				null, 
				configuration.getLanguageDriver(null));
	}
	
	public List<MybatisColumn> getColumns(String property) {
		return getColumns(property, "");
	}
	
	public List<MybatisColumn> getColumns(String property, String path) {
		return MybatisColumn.getColumns(getConfiguration().getTypeHandlerRegistry(), this.getEntityInformation().getJavaType(), property, path);
	}
	
	private void init() {
		String source = new StringBuilder()
			.append(this.getRepositoryInformation().getRepositoryInterface().getName().replaceAll("\\.", "/"))
			.append(".java (best guess)").toString();
		assistant = new MapperBuilderAssistant(this.configuration, source);
		assistant.setCurrentNamespace(this.getRepositoryInformation().getRepositoryInterface().getName());
	}
	
	protected void initEntityResultMap() {
		buildResultMap(entityInformation.getJavaType());
		this.configuration.parseXMLMapperBuilder(assistant.getCurrentNamespace());
	}
	
	protected Set<String> mapIds = new HashSet<String>();
	
	protected String buildResultMap(Class<?> entityType) {
		String id = entityType.getSimpleName();
		String mapid = assistant.applyCurrentNamespace(id, false);
		if(mapIds.contains(mapid)) {
			return mapid;
		}
		mapIds.add(mapid);
		List<ResultMapping> resultMappings = this.buildResultMappings(entityType);
		ResultMapResolver resultMapResolver = new ResultMapResolver(assistant, id, entityType, null, null, resultMappings, true);
		return resultMapResolver.resolve().getId();
	}
	
	protected List<ResultMapping> buildResultMappings(Class<?> entityType) {
		List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
		final Field[] fields = FieldUtils.getAllFields(entityType);
		for(Field field: fields) {
			if(Modifier.isStatic(field.getModifiers())) {
				continue;
			}
			if(field.getAnnotation(Transient.class) != null) {
				continue;
			}
			resultMappings.add(buildResultMapping(entityType, field));
		}
		return resultMappings;
	}
	
	public ResultMapping buildResultMapping(Class<?> entityType, Field field) {
		String property = field.getName();
		String column = MybatisUtils.toSqlColumnName(property);
		
		Class<?> javaTypeClass = field.getType();
		JdbcType jdbcTypeEnum = null;
		String nestedSelect = null;
		String nestedResultMap = null; //子节点
		String notNullColumn = null;
		String columnPrefix = null;
		Class<? extends TypeHandler<?>> typeHandlerClass = null;
		String resultSet = null;
		String foreignColumn = null;
		boolean lazy = configuration.isLazyLoadingEnabled();
		Column columnAnnotation = field.getAnnotation(Column.class);
		if(columnAnnotation != null) {
			if(StringUtils.isNotEmpty(columnAnnotation.value())) {
				column = columnAnnotation.value();
			}
			jdbcTypeEnum = columnAnnotation.jdbcType();
			nestedSelect = getNullAttr(columnAnnotation.select());
			nestedResultMap = getNullAttr(columnAnnotation.resultMap());
			notNullColumn = getNullAttr(columnAnnotation.notNullColumn());
			columnPrefix = getNullAttr(columnAnnotation.columnPrefix());
			resultSet = getNullAttr(columnAnnotation.resultSet());
			foreignColumn = getNullAttr(columnAnnotation.foreignColumn());
			lazy = StringUtils.isNotBlank(columnAnnotation.fetchType()) ? StringUtils.equals("lazy", columnAnnotation.fetchType()) : lazy;
			typeHandlerClass = columnAnnotation.typeHandler() != NullTypeHandler.class ? columnAnnotation.typeHandler() : null;
		}
		List<ResultFlag> flags = new ArrayList<ResultFlag>();
		if(field.getAnnotation(Id.class) != null) {
			flags.add(ResultFlag.ID);
		}
		if(StringUtils.isBlank(nestedResultMap)) {
			if(StringUtils.isBlank(nestedSelect)) {
				if(!configuration.getTypeHandlerRegistry().hasTypeHandler(javaTypeClass)) {
					Class<?> type = javaTypeClass;
					if(Collection.class.isAssignableFrom(javaTypeClass)) {
						ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
						type = (Class<?>) parameterizedType.getActualTypeArguments()[0];
					}
					nestedResultMap = this.buildResultMap(type);
				}
			}
		}
		return assistant.buildResultMapping(entityType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
	}
	
	private String getAttribute(String value, String defaultValue) {
		return StringUtils.isBlank(value) ? defaultValue : value;
	}
	
	private String getNullAttr(String value) {
		return getAttribute(value, null);
	}

	public List<MybatisColumn> getEntityColumns() {
		return MybatisColumn.getMybatisColumns(
				getConfiguration().getTypeHandlerRegistry(), 
				getEntityInformation().getJavaType());
	}
	
	public List<MybatisColumn> getEntityColumns(String prefix) {
		return MybatisColumn.getMybatisColumns(
				getConfiguration().getTypeHandlerRegistry(), 
				getEntityInformation().getJavaType(), prefix);
	}

	public GenerationManager getGenerationManager() {
		return generationManager;
	}
	
}
